package com.noahsloan.nutils.bytes;

import static java.lang.Math.max;
import static java.lang.Math.min;

import java.io.Closeable;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.BufferOverflowException;

/**
 * Essential this is a byte array queue. But since it is only for bytes, it is more
 * efficient than other Java implementations would be (we can say this because
 * other than simple arithmetic, this class only invokes native code). This
 * implementation is also thread safe, so the buffer can be shared.
 * <p>
 * In addition, this class can automatically increase its capacity when it
 * overflows. The size of this buffer can also be decreased via the
 * {@link #resize(int)} method, although it will throw an exception if unread
 * data would be truncated by the resize.
 * <p>
 * This buffer does not implement blocking reads, however, any Thread that
 * synchronizes and waits on this Object will be awakened by a call to one of
 * the read/write methods. i.e. the read/write methods invoke
 * {@link #notifyAll()}.
 * <p>
 * The advantage to using a ByteBuffer over a {@link PipedInputStream} and
 * {@link PipedOutputStream} is that reads and writes to a ByteBuffer are
 * non-blocking. Also, it is possible for multiple threads to read/write to this
 * buffer since reads and writes are atomic.
 * <p>
 * If blocking I/O is desired, see
 * {@link #blockingRead(ByteBuffer, int, int, byte[])} and
 * {@link #blockingWrite(ByteBuffer, int, int, byte[])}.
 * 
 * @author noah
 */
public class ByteBuffer implements Cloneable, Closeable {

    /**
     * Internal data store. Wraps around.
     */
    protected byte[] buffer;

    /**
     * Index of the first unread byte
     */
    protected int start;

    /**
     * The number of unread bytes in this buffer.
     */
    protected int fill;

    /**
     * Does this ByteBuffer automatically increase its own size when it
     * overflows?
     */
    public final boolean autoResize;

    /**
     * Has this buffer been shutdown?
     */
    private volatile boolean closed;

    /**
     * Creates an automatically resizing ByteBuffer with an initial capacity of
     * 1 byte.
     */
    public ByteBuffer() {
        this(1, true);
    }

    /**
     * Creates a non-resizing byte buffer of the given size.
     * 
     * @param size
     */
    public ByteBuffer(int size) {
        this(size, false);
    }

    /**
     * Creates a byte buffer of the given size that may or may not resize itself
     * when filled.
     * 
     * @param size
     * @param autoResize
     *            whether this buffer should resize itself when it exceeds its
     *            capacity.
     */
    public ByteBuffer(int size, boolean autoResize) {
        this.buffer = new byte[size];
        this.autoResize = autoResize;
    }

    /**
     * @return a ByteBuffer with the same capacity and data.
     */
    @Override
    protected synchronized Object clone() {
        ByteBuffer clone = new ByteBuffer(buffer.length);
        read(clone.buffer);
        clone.fill = fill;
        return clone;
    }

    /**
     * Read a byte from this buffer.
     * 
     * @return the byte, or -1 if no byte is available.
     */
    public synchronized int read() {
        if (fill == 0) {
            return -1;
        }
        byte b = buffer[start];
        start = (start + 1) / buffer.length;
        fill--;
        notifyAll();
        return b;
    }

    /**
     * Same as read(b,0,b.length)
     * 
     * @param b
     *            the buffer into which the data is read.
     * @return the number of bytes read
     */
    public synchronized int read(byte[] b) {
        return read(b, 0, b.length);
    }

    /**
     * Operates on the same contract as
     * {@link InputStream#read(byte[], int, int)}. The difference is that if
     * buff is >= the value returned by {@link #getSize()}, then it will always
     * be completely filled. Also, this method will not throw an IOException.
     * Finally, this method is non-block and will never return -1, but may
     * repeatedly return 0.
     * 
     * @param buff
     *            the buffer into which the data is read.
     * @param offset
     *            the start offset in array <code>buff</code> at which the
     *            data is written.
     * @param length
     * @return the number of bytes read into buff.
     */
    public synchronized int read(byte[] buff, int offset, int length) {
        if (fill == 0) {
            return 0;
        }
        // we start by splitting the array into two parts
        // 1) all bytes from the head of the queue to the end of the backing
        // array [start..length]
        // 2) all bytes before the head of the queue in the backing array
        // [0..start)

        // for 1), we want the smallest of one of the following:
        // a) the capacity of buff from offset to the end of the array
        // b) the number of unread bytes stored in this ByteBuffer
        // c) the number of bytes from the start to the end of the buffer array
        int firstHalf = min(length, min(fill, buffer.length - start));
        System.arraycopy(buffer, start, buff, offset, firstHalf);

        // for 2), we want to copy any unread data that resides in the first
        // half of the array, which is the minimum of:
        // a) the remaining capacity of buff
        // b) the number of bytes in 2) that are unread buffered data
        // if either of these are negative, we clamp to 0 and copy no bytes
        int secondHalf = max(0, min(length - firstHalf, fill
                - (buffer.length - start)));
        System.arraycopy(buffer, 0, buff, firstHalf, secondHalf);

        // finally, we move start ahead, and decrement fill by, the number of
        // bytes read
        int read = firstHalf + secondHalf;
        start = (start + read) % buffer.length;
        fill -= read;
        notifyAll();
        return read;
    }

    /**
     * The number of unread bytes residing in this buffer.
     * 
     * @return the number of unread bytes residing in this buffer.
     */
    public int getFill() {
        return fill;
    }

    /**
     * The capacity of this buffer.
     * 
     * @return the size of this buffer, in bytes.
     */
    public int getSize() {
        return buffer.length;
    }

    /**
     * 
     * @param newSize
     * @throws ArrayIndexOutOfBoundsException
     *             if the new size of the array is too small to accommodate the
     *             unread contents of this buffer.
     */
    public synchronized void resize(int newSize)
            throws ArrayIndexOutOfBoundsException {
        if (newSize < fill) {
            throw new ArrayIndexOutOfBoundsException("newSize is insufficient"
                    + " to hold this buffer's contents");
        }
        byte[] newBuff = new byte[newSize];
        read(newBuff);
        buffer = newBuff;
    }

    /**
     * Place a byte into this buffer. Will also notify all threads waiting on
     * this buffer.
     * 
     * @param b
     * @throws BufferOverflowException
     *             if this buffer is not set to automatically resize itself and
     *             it is already at full capacity
     */
    public synchronized void write(int b) {
        preWriteCheckSize(1);
        int index = (start + fill) % buffer.length;
        buffer[index] = (byte) b;
        fill++;
        // notify any waiting threads
        notifyAll();
    }

    /**
     * Writes all of the given bytes to this buffer.
     * 
     * @param b
     */
    public synchronized void write(byte[] b) {
        write(b, 0, b.length);
    }

    /**
     * Operates on the same contract as
     * {@link OutputStream#write(byte[], int, int)}. Will notify all threads
     * synchronized and waiting on this buffer.
     * 
     * @param b
     * @param offset
     * @param length
     * @throws BufferOverflowException
     *             if the buffer cannot accommodate the data to be written
     */
    public synchronized void write(byte[] b, int offset, int length)
            throws BufferOverflowException {
        int size = length - offset;
        // guarantee that there is sufficient buffer space
        preWriteCheckSize(size);
        // once again we must split an array into two halves:
        // 1) the bytes of b that will fit in buffer[start+fill..end]
        // 2) the remaining bytes that will have to be copied into
        // buffer[0..start)

        int end = start + fill; // don't mod yet
        // for 1) we need to know the minimum of:
        // a) the number of bytes to be written from b
        // b) the number of bytes between the end of the unread data and the end
        // of the buffer (this could be zero if the end index < start)
        int count1 = max(0, min(size, buffer.length - end));
        System.arraycopy(b, offset, buffer, end, count1);
        // for 2) we only need to know:
        // a) the end index of the already written data
        // b) the number of bytes remaining to be written
        end = (end + count1) % buffer.length;
        int count2 = max(0, size - count1);
        System.arraycopy(b, offset + count1, buffer, end, count2);
        // notify threads waiting on this buffer
        notifyAll();
    }

    /**
     * Checks that the given number of bytes will not overflow the internal
     * buffer.
     * <p>
     * If it will, then one of two things can happen.
     * <ol>
     * <li>If this buffer is set to automatically resize itself, then this
     * method will double its current size or make it large enough to
     * accommodate all the data to be written.
     * <li>If autoResize is not set, then it will throw a
     * {@link BufferOverflowException}.
     * 
     * @param bytes
     *            the number of bytes about to be written
     * @throws BufferOverflowException
     *             see main comment
     */
    private void preWriteCheckSize(int bytes) throws BufferOverflowException {
        if (fill + bytes > buffer.length) {
            if (autoResize) {
                resize(max(buffer.length * 2, fill + bytes));
            } else {
                throw new BufferOverflowException();
            }
        }
    }

    /**
     * Utility method for performing a blocking read.
     * 
     * @param buffer
     *            the ByteBuffer to read from.
     * @param length
     *            the number of bytes that must be read
     * @param offset
     *            the offset into b.
     * @param b
     *            the byte array to read the data into.
     */
    public static void blockingRead(ByteBuffer buffer, int length, int offset,
            byte[] b) {
        synchronized (buffer) {
            int read = 0;
            // loop until the buffer is full
            while (read < length && !buffer.isClosed()) {
                if (buffer.getFill() == 0) {
                    // if the buffer has no data, wait until some is written
                    try {
                        buffer.wait();
                    } catch (InterruptedException e) {
                    }
                }
                read += buffer.read(b, offset, length - read);
            }
            // wake anyone else that is waiting
            buffer.notifyAll();
        }
    }

    /**
     * Utility method for use with a non-resizing ByteBuffer. Will block until
     * the write has finished, without causing the buffer to overflow.
     * 
     * @param buffer
     *            the ByteBuffer to write to
     * @param offset
     *            the offset into b.
     * @param length
     *            the number of bytes of b to write.
     * @param b
     *            the byte buffer to write the bytes from.
     */
    public static void blockingWrite(ByteBuffer buffer, int offset, int length,
            byte[] b) {
        synchronized (buffer) {
            int written = 0;
            // loop until all bytes have been written
            while (written < length && !buffer.isClosed()) {
                int free = buffer.getSize() - buffer.getFill();
                if (free == 0) {
                    // if there is no room in the buffer, wait till there is
                    try {
                        buffer.wait();
                    } catch (InterruptedException e) {
                    }
                } else {
                    // fill as much of the buffer as is available and that we
                    // have data to yet write
                    int wrote = min(free, length - written);
                    buffer.write(b, offset, wrote);
                    written += wrote;
                }
            }
            // wake anyone else that is waiting
            buffer.notifyAll();
        }
    }

    /**
     * Close this buffer. This indicates that no more input is coming.
     */
    public synchronized void close() {
        this.closed = true;
        // there might be blocking I/O still waiting
        notifyAll();
    }

    /**
     * Has this buffer been closed? This is necessary so that any threads
     * waiting for input to this buffer can learn that it is not coming.
     */
    public boolean isClosed() {
        return closed;
    }
}
